/* 原型链关系图 */
// 或者记住一句话，new 关键字所形成的原型链关系是：实例.__proto__ === 构造函数.prototype
function Person() {
  this.brain = 'smart'
}

Person.prototype.getBrain = function () {
  console.log(this.brain)
}

Person.prototype.age = 100
Person.prototype.like = {
  color: 'red',
}

function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
}

JoestarFamily.prototype = new Person()
// 等同于 JoestarFamily.prototype.__proto__ === 实例.__proto__ === Person.prototype
JoestarFamily.prototype.constructor = JoestarFamily // 原型的 constructor 指回原来的构造函数
// 加一段的目的是为了让 JoestarFamily.prototype 的构造器属性指回 JoestarFamily，因为之前JoestarFamily.prototype = new Person()，
// 将 JoestarFamily.prototype 上默认的属性 constructor 弄没了，所以要指回来，不然它将无法调用 JoestarFamily.prototype 上的属性
var johnny = new JoestarFamily('johnny')
// 等同于 johnny.__proto__ === JoestarFamily.prototype
// 也就是说 johnny.__proto__.__proto__ === Person.prototype
var elaine = new JoestarFamily('elaine')

console.log(johnny, elaine)
// 也许你会感到奇怪，怎么 JoestarFamily 无了，
// 因为 JoestarFamily.prototype 直接赋值给了 new Person，
// 其 JoestarFamily.prototype 上的 constructor 属性也没了

// 在原型链继承中，如果原型上有对象（如like对象），所有实例都会跟着修改：

johnny.age = 1
console.log(johnny) // 1
console.log(elaine.age) // 100

johnny.like.color = 'yellow'
console.log(johnny.like.color) // yellow
console.log(elaine.like.color) // yellow

/* 2 借用构造函数继承 */
function Person(brain) {
  this.brain = brain

  this.others = {
    other1: 1,
    other2: 2,
  }
  this.setBrain = function () {
    console.log('set brain')
  }
}

Person.prototype.getBrain = function () {
  console.log(this.brain)
}

Person.prototype.age = 100
Person.prototype.like = {
  color: 'red',
}

function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
  Person.call(this, 'smart')
}

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')

console.log(johnny, elaine)
// 借用构造函数继承是一种「拿来主义」，JoestarFamily 和 Person 相互独立，
// JoestarFamily 只是那了（Call） Person 构造函数中的属性和方法。
function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }

  this.brain = 'smart'

  this.others = {
    other1: 1,
    other2: 2,
  }
  this.setBrain = function () {
    console.log('set brain')
  }
}
// 如此一来，两个实例的属性互不干扰，就不存在修改原型链上的对象值而影响到其他实例

johnny.others.other1 = 123
console.log(johnny.others.other1) // 123
console.log(elaine.others.other1) // 1
// 注意：所谓继承，是继承父类属性和方法。如果你在子类原型上添加对象属性，并修改对象属性中的某值，照样会影响所有的实例
// 但子类实例 johnny 和 elaine 却无法继承 Person.prototype 上的属性和方法（毕竟没有继承，只是拿了 Person 中的属性和方法），如下：

johnny.getBrain() // Uncaught TypeError: johnny.getBrain is not a function
johnny.age // undefined

/* 3 原型链+借用构造函数的组合继承 */

function Person(brain) {
  this.brain = brain

  this.others = {
    other1: 1,
    other2: 2,
  }
  this.setBrain = function () {
    console.log('set brain')
  }
}

Person.prototype.getBrain = function () {
  console.log(this.brain)
}

Person.prototype.age = 100
Person.prototype.like = {
  color: 'red',
}

function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
  Person.call(this, 'smart')
}

JoestarFamily.prototype = new Person()
// 等同于 JoestarFamily.prototype  === 实例.__proto__ === Person.prototype
JoestarFamily.prototype.constructor = JoestarFamily // 原型的 constructor 指回原来的构造函数

JoestarFamily.prototype.sayHello = function () {}

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')

console.log(johnny, elaine)
/* 
它与原型链继承比：因为在子类构造函数中调用 call 获取到父类构造函数中的属性（借用构造函数继承），所以实例化时会现在自身属性上找，这些值是独一份的；

johnny.others.other1 = 123
console.log(johnny.others.other1); // 123
console.log(elaine.others.other1); // 1
与借用构造函数继承比：JoestarFamily 的原型继承了 Person 的原型，能使用 Person 原型上的属性和方法

johnny.getBrain() // smart
johnny.age // 100
这种方式结合了原型链继承和借用构造函数继承的优点，是 JavaScript 中最常见的继承模式，不过也存在缺点，就是无论什么时候都会调用两次父类构造函数

一次是设置子类原型时：

JoestarFamily.prototype = new Person();
一次是创建子类实例的时候：

var johnny = new JoestarFamily('johnny')
//  Person.call(this, "smart")
 */

/* 4 原型式继承 如图一 */
// 原型 曾介绍过，原型继承分为显式原型继承和隐式原型继承，
// 隐式原型继承是语言内部帮我们实现，
// 而显式原型继承则需要我们动手实现

// Object.create

function Person(brain) {
  this.brain = brain

  this.others = {
    other1: 1,
    other2: 2,
  }
  this.setBrain = function () {
    console.log('set brain')
  }
}

Person.prototype.getBrain = function () {
  console.log(this.brain)
}

Person.prototype.age = 100
Person.prototype.like = {
  color: 'red',
}

function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
}

JoestarFamily.prototype = Object.create(Person.prototype) // 重新赋值原型，原先 JoestarFamily.prototype 上的 constructor 被抹除

JoestarFamily.prototype.constructor = JoestarFamily // 指回构造函数

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')
console.log(johnny, elaine)

/* 关键在于 Object.create 和 new 的不同

new带来的原型链关系是：实例.__proto__ === 构造函数.prototype
Object.create 的则是：实例.__proto__ === 传入的对象 */
// 这个案例中传入的对象是 Person.prototype，所以就有了 johnny.__proto__ === Person.prototype，它继承自传入的对象，而不像前三种继承，继承构造函数的原型（因为 new）

console.log(johnny.__proto__) // === Person.prototype
console.log(johnny.others.other1) // Cannot read properties of undefined (reading 'other1')
// 如果我们传入Person，会有另一番奇妙关系
// 所以经过 Object.create 创建的对象，它能继承传入对象的属性和方法

// 上诉例子中使用构造函数，构造函数中的属性和方法由 this 控制，所以没法被继承
/* 优点：

易于理解继承
缺点：

原型重新赋值后需要将属性 constructor 重新赋值回来
不能实现（子）类与（父）类的继承，只能实现对象的继承
原型对象的引用属性会被多实例共享，不管是私有还是共有属性
构造函数中的属性和方法无法被继承 */

/* 5 Object.setPrototypeOf  如图一 */
function Person(brain) {
  this.brain = brain

  this.others = {
    other1: 1,
    other2: 2,
  }
  this.setBrain = function () {
    console.log('set brain')
  }
}

Person.prototype.getBrain = function () {
  console.log(this.brain)
}

Person.prototype.age = 100
Person.prototype.like = {
  color: 'red',
}

function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
}

Object.setPrototypeOf(JoestarFamily.prototype, Person.prototype)

var johnny = new JoestarFamily('johnny')
var elaine = new JoestarFamily('elaine')
console.log(johnny, elaine)

/* 
它与 Object.create 不同的是，它能传入两个对象，这样，我们就能让子类原型继承自父类原型，实现继承

优点：

易于理解继承
缺点：

原型对象的引用属性会被多实例共享，不管是私有还是共有属性
构造函数中的属性和方法无法被继承
 */

/* 6 寄生式继承 */
// 创建一个函数，该函数内部以某种方式增强对象，最后返回对象

function createObj(obj) {
  var clone = Object.create(obj)
  clone.sayHello = function () {
    console.log('hello')
  }
  return clone
}
let person = {
  name: 'johnny',
  age: 22,
}

let anotherPerson = createObj(person)
anotherPerson.sayHello()
/* 原型式继承的一种拓展

优点：

无
缺点：

只是适用对象，与构造函数继承无关
和借用构造函数模式一样，无法实现函数复用，每次创建对象都会创建一遍方法 */

/* 7 寄生组合式继承 */
function Person(brain) {
  this.brain = brain

  this.others = {
    other1: 1,
    other2: 2,
  }
  this.setBrain = function () {
    console.log('set brain')
  }
}

Person.prototype.getBrain = function () {
  console.log(this.brain)
}

Person.prototype.age = 100
Person.prototype.like = {
  color: 'red',
}

function JoestarFamily(name) {
  this.name = name
  this.sayName = function () {
    console.log(this.name)
  }
  Person.call(this, 'smart')
}

var F = function () {} // 核心代码
F.prototype = Person.prototype // 核心代码

JoestarFamily.prototype = new F()

JoestarFamily.prototype.constructor = JoestarFamily // 原型的 constructor 指回原来的构造函数

JoestarFamily.prototype.sayHello = function () {}

var johnny = new JoestarFamily('johnny')

console.log(johnny)
/* 
不同的是，它少了因为 JoestarFamily 的原型上少了因 new Person 而产生的 brain、others 、setBrain 等 Person 构造函数的内置属性和方法

其实现的秘诀在于这几行代码

var F = function () {} // 创建一个空构造函数
F.prototype = Person.prototype // 将Person原型赋值给空构造函数的原型
// 即 F.prototype 拥有了 Person.prototype 上所有的属性和方法，包括 constructor，getBrain，age，like，__proto__
JoestarFamily.prototype = new F()
// new F，等于JoestarFamily.prototype.__proto__ === F.prototype
这个方法就是引用了 Object.create 的核心代码，其本质是不用 new 的特性，而是用显式原型继承的法子，这样就不用因使用 new 而产生副作用

显式原型继承不止一种，你 Object.create 能做的，我 Object.setPrototypeOf 也能实现

Object.setPrototypeOf(JoestarFamily.prototype, Person.prototype)

- var F = function () {} 
- F.prototype = Person.prototype 
- JoestarFamily.prototype = new F()
注意到没有，new 是会有副作用的，它不仅会建立原型链关系，而且会执行构造函数中的代码，将其赋予内存中生成的一个对象，并返回它成为实例

而像显式原型继承则只做关系（原型链）的链接，比较纯粹
 */
/* 
注意到没有，new 是会有副作用的，它不仅会建立原型链关系，而且会执行构造函数中的代码，将其赋予内存中生成的一个对象，并返回它成为实例

而像显式原型继承则只做关系（原型链）的链接，比较纯粹

优点：同组合继承

缺点：暂无
*/

/* 8 类继承 */
class Person {
  constructor(brain) {
    this.brain = brain
    this.others = {
      other1: 1,
      other2: 2,
    }
    this.setBrain = function () {
      console.log('set brain')
    }
  }
  getBrain() {
    console.log(this.brain)
  }
  age = 100
  like = {
    color: 'red',
  }
}

class JoestarFamily extends Person {
  constructor(name) {
    super('smart')
    this.name = name
    this.sayName = function () {
      console.log(this.name)
    }
  }
  sayHello() {}
}
var johnny = new JoestarFamily('johnny')

console.log(johnny)
/* 

class 继承相比传统继承（以寄生组合继承为对比），它的不同点是

构造函数也继承了：JoestarFamily.__proto__ === Person
父类原型对象的属性继承无法继承，如 Person.prototype.age 、Person.prototype.like
class 的职责是充当创建对象的模板，通常来说，data 数据由 instance 承载，而行为/方法则写在 class 里

也就是说，基于 class 的继承，继承的是行为和结构，但没有继承数据

而基于 prototype 的继承，则继承了数据、结构和行为三者

而为什么 class 不能继承数据呢？这是为了迎合 class 的基本行为，故意将其屏蔽

行为/方法可以公用，放在原型对象中，而数据则是独一份的，则可在构造函数中，这样就能解决大多数场景了，毕竟其他语言都是这样做的

*/

/* 
按照我们在 原型 中所说，继承可分为显式原型继承和隐式原型继承，显式原型继承分别是Object.create、Object.setPrototypeOf，隐式原型继承则是 new、对象字面量

这节我们从传统继承理解角度看，分别讲解了原型链继承、借用构造函数继承、组合继承（原型链+借用构造函数）、原型式继承（Object.create、Object.setPrototypeOf）、寄生式继承、寄生式组合继承（原型式+借用构造函数）、类继承等多种继承方式

也明白原型式继承就是显式原型继承

而与原型相关的继承如：原型链继承、Object.create、Object.setPrototypeOf 继承，有一个通病，即构造函数中的属性和方法无法被继承，，并且它们原型对象的引用属性会被实例共享

而唯一的解决方案就是借用构造函数继承，即在子类构造函数中调用 this 指针，而组合继承和寄生组合继承都能实现完美的原型链关系，两者的区别在于组合继承调用 了两次构造函数，其原因是因为 new 的副作用，而寄生组合继承能胜过一筹的原因就是显式原型继承是不会产生副作用，只做简单的原型关系关联

虽然现在原型的知识点在前端已显得不那么重要，理由也很简单，JavaScript 的各种框架开始抛弃混入模式，而转向了组合模式（把方法提取到独立的类和辅助对象中，然后把它们组合起来，但不使用继承）。”组合胜过继承“的设计模式已经让很多人遵循了，没必要理解原型也很正常，组合模式的运行也让函数式编程开始流行
*/
